...loading
2025-01-10
안녕하세요 오랜만에 기능을 추가하여 일지를 올립니다. 저는 이 블로그가 제 이야기에 대한 하나의 소통구가 되었음 좋겠다고 생각합니다. 저의 이야기를 업로드 하는 것도 중요하지만, 궁극적으로는 제가 업로드하는 이야기를 바탕으로 방문자들과 소통하는 것이 중요하다고 생각해서 댓글 기능을 고려하고 있었습니다. 나중으로 미루고 있었는데, 최근 어떤 유튜브 채널을 보면서 댓글창이 참 유익하게 활용되는 것을 봤습니다. 철학에 관련한 유튜브 채널인데 댓글에서 인상적인 커뮤니케이션이 이루어지고 있더군요.. 이를 보면서 '아, 나도 이러한 공간을 만들고 싶었지'라는 생각이 들어 개발에 들어갔습니다.
저는 방문자 분들이 최대한 편하게 댓글을 남기길 바랬습니다. 개인적인 생각이지만, 사용자를 불편하게 하는게 로그인이라 생각합니다.. 제가 로그인 기능을 만들거나 타 라이브러리를 사용한다면 댓글 기능을 보다 간편하게 구현할 수 있습니다. 다만 사용자가 댓글 만을 위해 로그인을 한다는 것은 상당히 불편할 수 있습니다. 따라서 로그인 없이 누구나 댓글을 추가할 수 있도록 햇습니다. 다만 각 댓글에 비밀번호 설정은 필요합니다. 작성자가 수정을 원할 수 있기 때문이죠.
댓글창을 최대한 깔끔하게 보여줄 수 있는 방법이 무엇일까 고민하면서 여기저기 레퍼런스를 많이 참고했습니다. 기존 디자인에 어울리면서도 심플한.. 그런 댓글창 디자인이 중요했습니다. 특히 댓글을 달때 여러 인풋창이 보이면 제가 보기에는 사실 많이 구립니다.. 이러한 점에 공감한 사람이 있었는지 애니메이션으로 이를 완화한 레퍼런스가 있더군요. 덕분에 이를 차용하여 댓글창을 아름답게 만들 수 있었습니다..
로그인 기능이 없어, 프로필 이미지가 없다면 댓글 작성자를 구분할 수 있는 유일한 기준이 작성자 이름입니다. 이러한 점들을 고려하여.. 댓글의 프로필 이미지를 커스텀하거나 선택할 수 있도록 디자인했습니다. 보시는 바와 같이 제 취향이 담긴 캐릭터 이미지를 선택할 수 있습니다 ㅎㅎ 물론 작성자가 원하는 이미지도 이미지 주소를 통해 넣을 수 있습니다. 이것은 저의 Own..아이디어이기에 나름 뿌듯합니다
댓글의 데이터 스키마에 대댓글 필드를 하나 추가하였고, 관리자에 한해서 1회의 대댓글만 작성하도록 만들었습니다. 온전한 대댓글 기능을 구현하기 위해서는 사실 댓글의 고유 ID를 참조할 수 있는 고유의 대댓글 객체들을 만들어야 하지만, 이 과정은 추후 보완을 위해 미루어 두었습니다. 아래는 댓글 객체의 스키마를 타입화한 객체입니다. 각 댓글 객체에서 대댓글 문자열이 관리되는 것을 확인할 수 있을겁니다.
export type CommentProps = { _id: string; articleTitle: string; date: string; comment: string; author: string; password: string; recomment: string; recommentDate: string; profileImageLink: string; };
댓글 작성/읽기/수정 코드는 개별적으로 첨부하지 않았습니다. 이전 포스팅에서도 다뤘던 간단한 CRUD기능이기 때문입니다. 다만 댓글 컴포넌트의 경우 브라우저의 기능을 활용해야만 하기 때문에 서버컴포넌트가 아닌 클라이언트 컴포넌트에서 데이터를 불러들이고 있습니다.
대댓글 기능은 최대한 심플하게 구현했습니다. 엄밀히 현재 대댓글 기능은 온전한 기능이라 할 수는 없습니다. 관리자인 저에 한해서 단 1회만 대댓글을 작성할 수 있도록 구현했기 때문입니다. 이 부분 꽤 고민했습니다.. 방문자의 로그인 기능을 배제했기 때문에 대댓글 기능을 구현하기 위해서 개발적으로도 디자인로도 꽤 고려할 사항이 많다는 것을 깨닫게 되었습니다. 따라서 1차적으로는 lean하게 구현하기로 했습니다. 추후 댓글 참여자들이 많아질 경우 점차적으로 기능을 보완하려 합니다.
댓글의 유효성 처리를 해줍니다. 작성자가 댓글을 빈칸으로 낸다던가, 수정을 하려는데 비밀번호가 불일치할 때 영문도 모르게 아무것도 동작하지 않는다면 당황할 수 밖에 없습니다. 귀찮을 수도 있지만 이러한 부분들은 당연히 세심하게 처리해줘야 합니다.
export async function editComment(commentFormData: any) { try { const response = await fetch( `${process.env.NEXT_PUBLIC_URL}/api/comment/edit/edit-comment`, { method: "PATCH", body: JSON.stringify(commentFormData), headers: { "Content-Type": "application/json", }, } ); switch (response.status) { case 200: return { success: true, code: 200 }; case 401: return { success: false, code: 401 }; case 500: return { success: false, code: 500 }; default: return { success: false, code: 500 }; } } catch (error) { return { success: false, code: 500 }; } }
예시로 위는 댓글 수정을 담당하는 API입니다. API를 통해 반환받은 코드넘버에 따라 상응하는 메시지를 클라이언트에 표시할 수 있습니다. 저는 에러 코드에 따른 메세지를 반환하는 유효성 메세지 컴포넌트를 하나 만들어 작동시켰습니다. 결과는 아래와 같습니다.
작성자가 초기 설정한 비밀번호가 올바르지 않을 경우 401 에러 코드를 받게 됩니다. 위 결과는 401 코드를 반환받을 경우 '비밀번호가 일치하지 않습니다'라는 문자열을 반환하도록 작동한 결과입니다.
아, 그리고 기본이지만 작성자가 초기설정한 비밀번호는 물론 암호화처리가 되어있습니다. 아래는 댓글 작성 시 암호화처리를 하여 저장하고 있는 '댓글 작성 API'의 코드입니다. bycryptjs를 사용하고 있습니다.
// creating-comment-api import type { NextApiRequest, NextApiResponse } from "next"; import { connectDatabase } from "@/utils/db"; import { genSalt, hash } from "bcryptjs"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { let client; let salt = await genSalt(10); let hashedPassword = await hash(req.body.password, salt); // 암호화처리 req.body.password = hashedPassword; try { if (req.method === "POST") { client = await connectDatabase(); const db = client.db(process.env.DB_NAME); const collection = db.collection("comments"); await collection.insertOne(req.body); } } catch (error) { console.error("Error fetching data:", error); res.status(500).json({ message: "Internal Server Error" }); } finally { res.status(201).json({ message: "your post is inserted" }); if (client) client.close(); } }
작성자가 댓글을 달았는데, 자신의 댓글이 바로 업로드 되지 않는다면 기분이 어떨까요.. 댓글은 작성하지마자 바로 반영되는게 제맛입니다. 따라서 댓글이 업로드되면 이를 감지해서 새로운 댓글 데이터 목록을 불러와야 합니다. React-Query를 사용하면 참 쉽겠지만, 이번 프로젝트는 라이브러리에 의존하지 않기로 하였기에 직접 구현해봅니다.
구현 아이디어는 간단합니다. 댓글 리스트를 다시 불러오는 상황을 의미하는 전역 상태를 관리합니다. 그리고 해당 상태가 변경될 때 마다 댓글 리스트 API가 호출되도록 구현하는 겁니다. 그리고 댓글을 작성하거나 수정하는 상황에서 해당 전역 상태를 변경합니다.
아래와 같이 전역적으로 사용할 컨텍스트를 생성합니다. 해당 컨텍스트를 통해 댓글 리스트를 리패칭할 수 있는 상태들과 함수를 선언합니다. 그리고 이를 커스텀 훅으로 반환하여 원하는 컴포넌트에서 해당 상태에 접근할 수 있도록 합니다
// CommentRefetchContext.tsx "use client"; import { createContext, useCallback, useState, ReactNode } from "react"; type CommentRefetchContextType = { triggerRefetchCount: number; setTriggerRefetchCount: (value: number) => void; refreshComments: () => void; }; export const CommentRefetchContext = createContext< CommentRefetchContextType | undefined >(undefined); export default function CommentRefetchProvider({ children, }: { children: ReactNode; }) { const [triggerRefetchCount, setTriggerRefetchCount] = useState(0); const refreshComments = useCallback(() => { setTriggerRefetchCount((prev) => prev + 1); }, []); return ( <CommentRefetchContext.Provider value={{ triggerRefetchCount, setTriggerRefetchCount, refreshComments }} > {children} </CommentRefetchContext.Provider> ); } // hook "use client"; import { useContext } from "react"; import { CommentRefetchContext } from "@/providers/context/comment-refetch-provider"; export const useCommentRefetch = () => { const context = useContext(CommentRefetchContext); if (!context) { throw new Error( "useCommentRefetch must be used within a CommentRefetchProvider" ); } return context; };
triggerRefetchCount
: 댓글 리스트 리패치를 위한 전역상태입니다. 해당 상태는 숫자형으로 관리합니다. 댓글 리스트를 리패치 트리거가 발생할 때 마다 상태값이 증가되도록 합니다.
setTriggerRefetchCount
: 위 상태의 setter함수입니다.
refreshComments
: setter함수를 명시적으로 재선언한 함수입니다. 댓글 리스트 리패칭을 트리거할 때 refreshComments를 호출합니다. 또한 useCallback을 사용하여 해당 함수가 매번 재생성되지 않도록 만들었습니다.
이제 생성한 context를 사용할 수 있습니다. 우선 데이터를 불러오는 컴포넌트에서 위 Context에서 반환한 triggerRefetchCount를 불러옵니다. 해당 상태가 변경되면 자동으로 댓글 목록을 새로 불러와야합니다. 따라서 댓글 목록 API를 호출하고 있는 useEffect의 의존성에 triggerRefetchCount를 넣어줍니다. 이제 triggerRefetchCount가 변경되면 새로운 댓글 목록을 불러올 준비가 되었습니다.
// Comment-Component // ... import { useCommentRefetch } from "@/hooks/useCommentRefetch"; // ... export default function CommentComponent({ articleTitle, }: CommentContainerProps) { const { triggerRefetchCount } = useCommentRefetch(); const [commentsList, setCommentsList] = useState<CommentProps[]>([]); useEffect(() => { const fetchData = async () => { const response = await fetch(`/api/comment/get/${articleTitle}`, { next: { revalidate: STALE_TIME }, }); const result = await response.json(); if (result) setCommentsList(result); }; fetchData(); }, [triggerRefetchCount]); //...
이제 댓글을 작성할 때 위에서 선언한 triggerRefecthCount에 변경을 줘야합니다. 댓글을 제출하는 함수에서 성공적으로 댓글이 제출되었을때 triggerRefetchCount를 변경하는 setter함수를 호출합니다.
// Submit-Comment-Component import { useCommentRefetch } from "@/hooks/useCommentRefetch"; //... const { refreshComments } = useCommentRefetch(); const onClickSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const result = await submitNewComment(form); if (result.ok) { refreshComments(); } else { //... } } catch (error) { console.error("An error occurred:", error); } }; /...
결과는 성공적입니다. 보다 매끄러운 UX가 가능해졌습니다 ㅎ
댓글 기능을 완료했습니다. 블로그 프로젝트에서 가장 중요한 부분이 될거라고 내심 생각하고 있었는데, 생각보다 금방 기능을 완료하게 되었네요. 꽤 lean하게 댓글 기능을 구현한 것 같습니다. 지금 보다 더 정교한 대댓글 기능을 추가시키는 것은 조금 더 고려해봐야할 문제 인 것 같습니다. 블로그를 운영하며 필요성을 느끼게 되면 추가될 것 같습니다.
글을 적고나서 생각해보니 급급하게 글을 작성하느라 자세한 코드들을 제시해드리진 못한 것 같습니다. 혹시라도 궁금하신 부분이 있다면 이제 댓글 기능이 있으니 남겨주시면 더 자세하게 설명드리겠습니다!
Comments